﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using System.Threading;
using System.Diagnostics.Contracts;

namespace NCacheDServer.CacheStore
{
    internal sealed class Cache
    {
        private ConcurrentDictionary<string, CacheItem> cacheItems;
        private long totalSize = 0;

        private readonly long maxCacheSize;

        private const long maxCacheBuffer = 1000000;

        private readonly TimeSpan shrinkCacheInterval = new TimeSpan(0, 0, 30); // only clear cache every 30 seconds

        private DateTime cacheLastShrink;



        public Cache()
        {
            cacheItems = new ConcurrentDictionary<string, CacheItem>();

            long configCacheSize = long.Parse(ConfigurationManager.AppSettings.Get("CacheSize"));
            maxCacheSize = configCacheSize * 1000000; // config is MB

            cacheLastShrink = DateTime.Now;

        }

        public byte[] GetItem(string key)
        {
            Contract.Assert(key != null);
            CacheItem val;

            if (cacheItems.TryGetValue(key, out val) && val != null)
            {
                if (val.Expiration > DateTime.Now)
                {
                    val.LastAccessed = DateTime.Now;
                    val.IncrementAccessCount();
                    return val.Value;
                }
                else
                {
                    RemoveItem(key);
                }
            }
            return null;
        }

        public void PutItem(string key, DateTime expiration, byte[] value)
        {
            Contract.Assert(key != null);

            // if cache is full, we can't add anything else
            if (totalSize + value.Length > maxCacheSize)
            {
                return;
            }

            CacheItem item = new CacheItem(value, expiration);

            //Eventually consistant...  could use task factory to update size?
            cacheItems.AddOrUpdate(key, k =>
                {
                    Interlocked.Add(ref totalSize, value.Length);
                    return item;
                },
                (k, oldItem) =>
                {
                    long newSize = value.Length;
                    if (oldItem != null)
                        newSize -= oldItem.Value.Length;
                    Interlocked.Add(ref totalSize, newSize);
                    return item;
                });

            if (totalSize + maxCacheBuffer > maxCacheSize && cacheLastShrink + shrinkCacheInterval < DateTime.Now)
            {
                Task.Factory.StartNew(() => ShrinkCache());
            }
        }

        internal void RemoveItem(string key)
        {
            Contract.Assert(key != null);

            CacheItem item;
            if (cacheItems.TryRemove(key, out item))
            {
                if (item != null && item.Value != null)
                {
                    Interlocked.Add(ref totalSize, -item.Value.Length);
                }
            }
        }

        internal void ShrinkCache()
        {

            if (cacheLastShrink + shrinkCacheInterval < DateTime.Now)
            {
                cacheLastShrink = DateTime.Now;
                long sizeChange = 0;
                foreach (var item in cacheItems)
                {
                    if (item.Value != null && !item.Value.IsValid())
                    {
                        if (cacheItems.TryUpdate(item.Key, null, item.Value))
                        {
                            sizeChange -= item.Value.Value.Length;
                        }
                    }
                }
                Interlocked.Add(ref totalSize, sizeChange);
                cacheLastShrink = DateTime.Now;
            }
        }

        internal long GetMaxSize()
        {
            return maxCacheSize;
        }
    }
}
